using System;
using System.Collections.Generic;
using System.Text;
using AUSRIS.Reports.Utilities.Deployment.Entities;
using AUSRIS.Reports.Utilities.Deployment.FileSystem;
using AUSRIS.Reports.Utilities.Deployment.Logging;
using AUSRIS.Reports.Utilities.Deployment.Target;
using AUSRIS.Reports.Utilities.Deployment.Target.Tasks;
using AUSRIS.Reports.Utilities.Deployment.Configuration;

namespace AUSRIS.Reports.Utilities.Deployment
{
    /// <summary>
    /// This implementation of <see cref="IPublisher"/>
    /// creates a proposed plan of what it will publish
    /// and logs the plan before executing.
    /// It checks out a group of files (specified by module-name 
    /// in the <see cref="DeploymentSet"/>) and then 
    /// compares each file's revision with what was
    /// previously published to determine if it needs
    /// to publish.
    /// </summary>
    public class TaskDrivenPublisher : IPublisher
    {
        #region Private Fields

        private bool error;
        private IReportServer reportServer;
        private IRepository repository;
        private ILogger logger;
        private IConfigurationDatabase configurationDatabase;
        private DeploymentSet currentDeploymentSet;
        private IList<Report> publishedReportLog;
        private IList<Report> skippedReportLog;
        private IList<Report> reportsForParameterUpdate;
        private IList<IReportServerTask> cleanupTasks;
        private IList<IReportServerTask> folderPublishTasks;
        private IList<IReportServerTask> dataSourcePublishTasks;
        private IList<IReportServerTask> filePublishTasks;
        private IList<IReportServerTask> policyPublishTasks;
        #endregion

        #region Public Properties

        public ILogger Logger
        {
            get
            {
                return logger;
            }
            set
            {
                logger = value;
            }
        }

        /// <summary>
        /// Source repository to pull source control for reports from
        /// </summary>
        public IRepository Repository
        {
            get
            {
                return repository;
            }
            set
            {
                repository = value;
            }
        }

        /// <summary>
        /// Report Server to deploy reports to
        /// </summary>
        public IReportServer ReportServer
        {
            get
            {
                return reportServer;
            }
            set
            {
                reportServer = value;
            }
        }

        /// <summary>
        /// Configuration Database with settings for the reports deploy
        /// </summary>
        public IConfigurationDatabase ConfigurationDatabase
        {
            get
            {
                return configurationDatabase;
            }
            set
            {
                configurationDatabase = value;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Load the Deployment Set into the the TaskDrivenPublisher
        /// </summary>
        /// <param name="name">Name of the Deployment Set to Load</param>
        public void LoadDeploymentSet(string name)
        {
            this.Reset();
            this.Logger.LogMessage("Loading Deployment Set '{0}'", name);
            this.currentDeploymentSet = this.ConfigurationDatabase.GetDeploymentSet(name);
            if(this.currentDeploymentSet == null)
            {
                this.error = true;
                this.Logger.LogException(new Exception("Could not load Deployment Set: " + name));
            }

            this.InitializeReportServer();
            this.CheckReportServer();

            this.GetLocalFileCopies();

            this.CreateCleanupTasks();
            this.CreateFolderPublishTasks();
            this.CreateDataSourcePublishTasks();
            this.CreateFilePublishTasks();

            this.Logger.LogNewLine();
            this.Logger.LogHeader("Planned Tasks to execute:");
            this.LogPlannedTaskMessages();
            this.Logger.LogNewLine();
        }

        /// <summary>
        /// Executes all Task involved with publishing the report artifacts to the report server
        /// </summary>
        public void Publish()
        {
            if(this.currentDeploymentSet == null)
            {
                throw new InvalidOperationException("No Deployment Set Loaded");
            }
            this.BeginTransaction();
            this.ExecuteCleanupTasks();
            this.ExecuteFolderPublishTasks();
            this.ExecuteDataSourcePublishTasks();
            this.ExecuteFilePublishTasks();
            this.ExecutePolicyPublishTasks();
            this.CommitTransaction();
            this.UpdateReportParameters();
            this.UpdateModelDatasources();
            this.LogCompletionMessages();
            this.Logger.Flush();
        }

        #endregion

        #region Private Helper Methods

        /// <summary>
        /// Empties all the List Type members and sets the error member to false
        /// </summary>
        private void Reset()
        {
            this.skippedReportLog = new List<Report>();
            this.publishedReportLog = new List<Report>();
            this.cleanupTasks = new List<IReportServerTask>();
            this.folderPublishTasks = new List<IReportServerTask>();
            this.dataSourcePublishTasks = new List<IReportServerTask>();
            this.filePublishTasks = new List<IReportServerTask>();
            this.policyPublishTasks = new List<IReportServerTask>();
            this.reportsForParameterUpdate = new List<Report>();
            this.error = false;
        }

        /// <summary>
        /// 
        /// </summary>
        private void GetLocalFileCopies()
        {
            if(!error)
            {
                try
                {
                    this.Logger.LogNewLine();
                    this.Logger.LogMessage("Checking out local copies of {0}", this.currentDeploymentSet.ModuleName);
                    this.Repository.CheckoutModule(this.currentDeploymentSet.ModuleName, this.currentDeploymentSet.VersionTag);
                }
                catch(Exception ex)
                {
                    this.error = true;
                    this.Logger.LogException(ex);
                }
            }
        }

        /// <summary>
        /// Logs the "Publish Executed Successfully" message if no errors occur
        /// </summary>
        private void LogCompletionMessages()
        {
            if(!error)
            {
                this.Logger.LogHeader("Publish Executed Successfully");
                this.Logger.LogSuccess("Processed {0} report(s)", this.skippedReportLog.Count + this.publishedReportLog.Count);
                this.Logger.LogSuccess("Published {0} report(s)", this.publishedReportLog.Count);
                this.Logger.LogWarning("Skipped {0} report(s)", this.skippedReportLog.Count);
                if(this.publishedReportLog.Count > 0)
                {
                    this.Logger.LogNewLine();
                    this.Logger.LogMessage("The following reports were published:");
                    foreach(Report report in this.publishedReportLog)
                    {
                        this.Logger.LogSuccess(report.Path);
                    }
                }
            }
        }

        /// <summary>
        /// Attempts to initialize the report server and logs the outcome.
        /// </summary>
        private void InitializeReportServer()
        {
            if(!this.error)
            {
                this.Logger.LogMessage("Initializing report server {0} to {1}", this.ReportServer.ToString(), this.currentDeploymentSet.TargetServiceUrl);
                try
                {
                    this.ReportServer.Initialize(this.currentDeploymentSet.TargetServiceUrl);
                }
                catch(Exception ex)
                {
                    this.Logger.LogException(ex);
                    this.error = true;
                }
            }
        }

        /// <summary>
        /// Checks the report server's availability
        /// </summary>
        private void CheckReportServer()
        {
            if(!this.error)
            {
                this.Logger.LogMessage("Checking report server {0}", this.ReportServer.ToString());
                try
                {
                    if(!this.ReportServer.CanConnect())
                    {
                        this.Logger.LogWarning("Could not connect to report server");
                        this.error = true;
                    }
                }
                catch(Exception ex)
                {
                    this.Logger.LogException(ex);
                    this.error = true;
                }
            }
        }

        /// <summary>
        /// Starts the Publishing events or 'Transaction'
        /// </summary>
        private void BeginTransaction()
        {
            if(!error)
            {
                try
                {
                    this.Logger.LogNewLine();
                    this.Logger.LogMessage("Beginning Transaction");
                    this.ReportServer.BeginTransaction();
                }
                catch(Exception ex)
                {
                    this.Logger.LogException(ex);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private void CommitTransaction()
        {
            if(!error)
            {
                try
                {
                    this.Logger.LogNewLine();
                    this.Logger.LogSuccess("Committing Transaction");
                    this.ReportServer.CommitTransaction();
                }
                catch(Exception ex)
                {
                    this.Logger.LogException(ex);
                    this.Cancel();
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private void Cancel()
        {
            this.error = true;

            this.Logger.LogNewLine();
            this.Logger.LogWarning("Rolling Back Transaction");

            this.ReportServer.RollbackTransaction();

            this.Logger.LogNewLine();
            this.Logger.LogWarning("Publish Failed");
        }

        /// <summary>
        /// Test the <see cref="CatalogFile"/> passed in to determine if it is a <see cref="Report"/> or <see cref="Model"/> file
        /// and then sets the file's revision accordingly
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="file"></param>
        /// <returns></returns>
        private bool FileIsPublished(Folder folder, CatalogFile file)
        {
            Report report = file as Report;
            if(report != null)
            {
                string lastRevisionPublished = this.ReportServer.GetReportRevision(report);
                return lastRevisionPublished == report.Revision;
            }
            Model model = file as Model;
            if(model != null)
            {
                string lastRevisionPublished = this.ReportServer.GetModelRevision(model);
                return lastRevisionPublished == model.Revision;
            }
            return this.ReportServer.ItemExists(file);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="wasPublished"></param>
        private void LogItem(DeploymentItem item, bool wasPublished)
        {
            if(item.GetType().Equals(typeof(Report)))
            {
                if(wasPublished)
                {
                    this.publishedReportLog.Add((Report)item);
                }
                else
                {
                    this.skippedReportLog.Add((Report)item);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private void LogPlannedTaskMessages()
        {
            if(this.cleanupTasks.Count > 0)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Clean-up tasks:");
                this.LogPlannedTasks(this.cleanupTasks);
            }
            if(this.folderPublishTasks.Count > 0)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Folder publish tasks:");
                this.LogPlannedTasks(this.folderPublishTasks);
            }
            if(this.dataSourcePublishTasks.Count > 0)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("DataSource publish tasks:");
                this.LogPlannedTasks(this.dataSourcePublishTasks);
            }
            if(this.filePublishTasks.Count > 0)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("File publish tasks:");
                this.LogPlannedTasks(this.filePublishTasks);
            }
            if(this.policyPublishTasks.Count > 0)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Policy change publish tasks:");
                this.LogPlannedTasks(this.policyPublishTasks);
            }
        }

        #endregion

        #region Task Creation

        private void CreateCleanupTasks()
        {
            if(!this.error)
            {
                try
                {
                    this.Logger.LogNewLine();
                    this.Logger.LogMessage("Creating clean-up tasks");
                    IList<Folder> topFolders = this.currentDeploymentSet.Folders.GetTopFolders();
                    foreach(Folder topFolder in topFolders)
                    {
                        this.CreateFolderCleanupTasks(topFolder);
                    }
                }
                catch(Exception ex)
                {
                    this.Logger.LogException(ex);
                    this.Cancel();
                }
            }
        }

        private void CreateFolderCleanupTasks(Folder folder)
        {
            Folder deploymentFolder = null;
            if(this.currentDeploymentSet.Folders.ContainsKey(folder.Path))
            {
                deploymentFolder = this.currentDeploymentSet.Folders[folder.Path];
            }
            if(deploymentFolder == null)
            {
                this.cleanupTasks.Add(new DeleteTask(folder));
            }
            else
            {
                foreach(CatalogFile file in this.ReportServer.GetFiles(folder))
                {
                    if(!deploymentFolder.ContainsFile(file))
                    {
                        this.cleanupTasks.Add(new DeleteTask(file));
                    }
                }
                foreach(Folder subFolder in this.ReportServer.GetFolders(folder))
                {
                    this.CreateFolderCleanupTasks(subFolder);
                }
            }
        }

        private void CreateFolderPublishTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Creating Folder Publish Tasks");
                foreach(Folder folder in this.currentDeploymentSet.Folders.GetFlatList())
                {
                    try
                    {
                        if(!this.ReportServer.ItemExists(folder))
                        {
                            this.folderPublishTasks.Add(new FolderPublishTask(folder));
                        }
                        else
                        {
                            this.CreatePolicyPublishTask(folder);
                        }
                    }
                    catch(Exception ex)
                    {
                        this.Logger.LogException(ex);
                        this.Cancel();
                        break;
                    }
                }
            }
        }

        private void CreateDataSourcePublishTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Creating Data Source Publish Tasks");
                foreach(Folder folder in this.currentDeploymentSet.Folders.GetFlatList())
                {
                    if(folder.Datasources.Count > 0)
                    {
                        foreach(DataSource dataSource in folder.Datasources)
                        {
                            try
                            {
                                if(dataSource.Overwrite || !this.reportServer.ItemExists(dataSource))
                                {
                                    this.dataSourcePublishTasks.Add(new DataSourcePublishTask(dataSource));
                                }
                            }
                            catch(Exception ex)
                            {
                                this.Logger.LogException(ex);
                                this.Cancel();
                                break;
                            }
                        }
                    }
                }
            }
        }

        private void CreateFilePublishTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Creating File Publish Tasks");
                foreach(Folder folder in this.currentDeploymentSet.Folders.GetFlatList())
                {
                    if(!error)
                    {
                        if(folder.Files.Count > 0)
                        {
                            this.Logger.LogHeader("Examining Files in '{0}'", folder.Path);
                            foreach(CatalogFile file in folder.Files)
                            {
                                this.Logger.LogNewLine();
                                this.Logger.LogMessage("Getting revision for '{0}'", file.Name);
                                try
                                {
                                    if(!this.Repository.FileExists(file))
                                    {
                                        this.Logger.LogWarning("File '{0}' not found", file.Name);
                                        this.LogItem(file, false);
                                        continue;
                                    }
                                    this.Repository.SetRevision(file);
                                    if(file.GetType().Equals(typeof(Model)))
                                    {
                                        this.Repository.SetDataSourceViewPath((Model)file);
                                    }
                                }
                                catch(Exception ex)
                                {
                                    this.Logger.LogException(ex);
                                    this.Cancel();
                                    break;
                                }
                                try
                                {
                                    if(this.FileIsPublished(folder, file))
                                    {
                                        this.Logger.LogWarning("File '{0}', Revision '{1}' is already published", file.Name, file.Revision);
                                        this.LogItem(file, false);
                                        this.CreatePolicyPublishTask(file);
                                    }
                                    else
                                    {
                                        this.Logger.LogSuccess("Creating File Publish Task for '{0}', Revision '{1}'", file.Name, file.Revision);
                                        this.filePublishTasks.Add(new FilePublishTask(folder, file));
                                    }
                                    if(file.GetType().Equals(typeof(Report)))
                                    {
                                        this.reportsForParameterUpdate.Add(file as Report);
                                    }
                                }
                                catch(Exception ex)
                                {
                                    this.Logger.LogException(ex);
                                    this.Cancel();
                                    break;
                                }
                            }
                        }
                    }

                }
            }
        }

        private void CreatePolicyPublishTask(DeploymentItem item)
        {
            bool needToPublish = false;
            IList<ItemPolicy> existingPolicies = this.ReportServer.GetItemPolicies(item);
            if(existingPolicies.Count != item.Policies.Count)
            {
                needToPublish = true;
            }
            else
            {
                List<ItemPolicy> existingList = new List<ItemPolicy>(existingPolicies);
                List<ItemPolicy> newList = new List<ItemPolicy>(item.Policies);
                existingList.Sort();
                newList.Sort();
                for(int x = 0; x < existingList.Count; x++)
                {
                    if(!existingList[x].Equals(newList[x]))
                    {
                        needToPublish = true;
                        break;
                    }
                }
            }
            if(needToPublish)
            {
                this.policyPublishTasks.Add(new PolicyPublishTask(item));
            }
        }

        private void ExecuteTasks(IList<IReportServerTask> tasks)
        {
            foreach(IReportServerTask task in tasks)
            {
                this.Logger.LogMessage("Executing Task: {0}", task);
                try
                {
                    task.Execute(this.ReportServer);
                }
                catch(Exception ex)
                {
                    this.Logger.LogException(ex);
                    this.Cancel();
                    break;
                }
                this.Logger.LogSuccess("Completed Task: {0}", task);
            }
        }

        private void LogPlannedTasks(IList<IReportServerTask> tasks)
        {
            foreach(IReportServerTask task in tasks)
            {
                this.Logger.LogMessage(task.ToString());
            }
        }

        #endregion

        #region Task Execution

        private void ExecuteCleanupTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Cleaning up items from server");
                this.ExecuteTasks(this.cleanupTasks);
            }
        }

        private void ExecuteFolderPublishTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Publishing Folders");
                this.ExecuteTasks(this.folderPublishTasks);
            }
        }

        private void ExecutePolicyPublishTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Publishing Policy Changes");
                this.ExecuteTasks(this.policyPublishTasks);
            }
        }

        private void ExecuteDataSourcePublishTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Publishing DataSources");
                this.ExecuteTasks(this.dataSourcePublishTasks);
            }
        }

        private void ExecuteFilePublishTasks()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Publishing Files");
                this.ExecuteTasks(this.filePublishTasks);

                if(!error)
                {
                    foreach(FilePublishTask task in this.filePublishTasks)
                    {
                        this.LogItem(task.File, true);
                    }
                }
            }
        }

        #endregion

        private void UpdateReportParameters()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Updating report parameters on server");
                foreach(Report report in this.reportsForParameterUpdate)
                {
                    try
                    {
                        this.ReportServer.UpdateReportParameters(report);
                    }
                    catch(Exception ex)
                    {
                        this.Logger.LogException(ex);
                    }
                    this.Logger.LogSuccess("Updated parameters for '{0}'", report.Name);
                }
            }
        }

        private void UpdateModelDatasources()
        {
            if(!error)
            {
                this.Logger.LogNewLine();
                this.Logger.LogMessage("Updating model datasources on server");
                foreach(FilePublishTask task in this.filePublishTasks)
                {
                    Model model = task.File as Model;
                    if(model != null)
                    {
                        try
                        {
                            this.ReportServer.UpdateModelDataSource(model);
                        }
                        catch(Exception ex)
                        {
                            this.Logger.LogException(ex);
                        }
                        this.Logger.LogSuccess("Updated datasource for '{0}'", model.Name);
                    }
                }
            }
        }
    }
}